﻿#region License

// This file is part of Grawlix.
// 
// Grawlix is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// Grawlix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public
// License along with Grawlix.  If not, see
// <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.Remoting;
using System.Security;
using System.Security.Permissions;
using System.Security.Policy;
using System.Threading;
using log4net;
using log4net.Config;

namespace Grawlix.Services
{
	/// <summary>
	/// An environment that encapsulates a separate 
	/// <see cref="AppDomain"/>.
	/// </summary>
	public class AppDomainEnvironment : IExecutionEnvironment
	{
		/// <summary>
		/// The log used to report errors that occur.
		/// </summary>
		private static readonly ILog _log =
			LogManager.GetLogger(
				"Grawlix.Services.AppDomainEnvironment");

		/// <summary>
		/// The <see cref="AppDomain"/> that this environment manages.
		/// </summary>
		private AppDomain _appDomain;

		/// <summary>
		/// Whether the object has been disposed of.
		/// </summary>
		private bool _disposed;

		// Methods
		/// <summary>
		/// Initializes a new instance of the 
		/// <see cref="AppDomainEnvironment"/> class.
		/// </summary>
		/// <param name="name">The name of the environment.</param>
		/// <param name="basePath">The base path to Grawlix assemblies.</param>
		/// <param name="searchPaths">The search paths to other assemblies.</param>
		/// <exception cref="ArgumentNullException"><c>searchPaths</c> is null.</exception>
		/// <exception cref="ArgumentNullException"><c>basePath</c> is null.</exception>
		/// <exception cref="ArgumentNullException"><c>name</c> is null.</exception>
		public AppDomainEnvironment(string name, string basePath,
		                            string searchPaths)
		{
			if (searchPaths == null)
				throw new ArgumentNullException("searchPaths");
			if (basePath == null)
				throw new ArgumentNullException("basePath");
			if (name == null)
				throw new ArgumentNullException("name");
			AppDomainSetup setup = CreateSetupData();
			setup.ApplicationName = name;
			setup.ApplicationBase = basePath;
			setup.PrivateBinPath = searchPaths;
			Assembly assembly = Assembly.GetExecutingAssembly();
			_appDomain = AppDomain.CreateDomain(name,
			                                    AppDomain.
			                                    	CurrentDomain.
			                                    	Evidence, setup,
			                                    FindPermissions(),
			                                    BuildStrongAssemblyName
			                                    	(assembly));
			_appDomain.SetData("PARENT_APPDOMAIN",
			                   AppDomain.CurrentDomain);
			Debug.Assert(!string.IsNullOrEmpty(assembly.FullName));
			_appDomain.Load(assembly.FullName);
			_appDomain.DoCallBack(StartLog);
		}

		/// <summary>
		/// Builds a strong assembly name for the specified assembly.
		/// </summary>
		/// <param name="assembly">The assembly whose name to build.
		/// </param>
		/// <returns>The strong name for the assembly.</returns>
		private static StrongName BuildStrongAssemblyName(
			Assembly assembly)
		{
			AssemblyName name = assembly.GetName();
			byte[] publicKey = name.GetPublicKey();
			Debug.Assert(publicKey != null && publicKey.Length > 0);
			return
				new StrongName(new StrongNamePublicKeyBlob(publicKey),
				               name.Name, name.Version);
		}

		/// <summary>
		/// Creates an instance of the specified type in the
		/// represented environment.
		/// </summary>
		/// <param name="type">The type of object to create.</param>
		/// <param name="arguments">The arguments to that object.
		/// </param>
		/// <returns>The new object.</returns>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed of.</exception>
		/// <exception cref="ArgumentNullException"><c>arguments</c> is
		/// null.</exception>
		/// <exception cref="ArgumentNullException"><c>type</c> is
		/// null.</exception>
		public object CreateInstance(Type type,
		                             params object[] arguments)
		{
			if (_disposed)
				throw new ObjectDisposedException("AppDomainEnvironment");
			if (arguments == null)
				throw new ArgumentNullException("arguments");
			if (type == null)
				throw new ArgumentNullException("type");
			Debug.Assert(
				!string.IsNullOrEmpty(type.Assembly.FullName));
			ObjectHandle wrapped =
				Activator.CreateInstance(_appDomain,
				                         type.Assembly.FullName,
				                         type.FullName, false,
				                         BindingFlags.Default, null,
				                         arguments, null, null, null);
			Debug.Assert(wrapped != null);
			object instance = wrapped.Unwrap();
			Debug.Assert(instance != null);
			Debug.Assert(instance.GetType() == type);
			return instance;
		}

		/// <summary>
		/// Creates the <see cref="AppDomainSetup"/> object for a new 
		/// <see cref="AppDomain"/>.
		/// </summary>
		/// <returns>The new setup information.</returns>
		private static AppDomainSetup CreateSetupData()
		{
			var setup = new AppDomainSetup
				{
					DisallowApplicationBaseProbing = false,
					DisallowBindingRedirects = true,
					DisallowCodeDownload = true,
					DisallowPublisherPolicy = true,
					LoaderOptimization = LoaderOptimization.MultiDomain
				};
			return setup;
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing,
		/// releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources
		/// </summary>
		/// <param name="disposing"><c>true</c> to release both managed
		/// and unmanaged resources; <c>false</c> to release only
		/// unmanaged resources.</param>
		protected virtual void Dispose(bool disposing)
		{
			if (!_disposed)
			{
				if (disposing)
					_appDomain = null;
				Unload();
				_disposed = true;
			}
		}

		/// <summary>
		/// Releases unmanaged resources and performs other cleanup
		/// operations before the
		/// <see cref="AppDomainEnvironment"/> is reclaimed by garbage
		/// collection.
		/// </summary>
		~AppDomainEnvironment()
		{
			Dispose(false);
		}

		/// <summary>
		/// Finds a good default set of permissions.
		/// </summary>
		/// <returns>A good default set of permissions.</returns>
		private static PermissionSet FindPermissions()
		{
			ApplicationTrust applicationTrust =
				AppDomain.CurrentDomain.ApplicationTrust;
			if (applicationTrust != null)
				return applicationTrust.DefaultGrantSet.PermissionSet;
			return
				SecurityManager.ResolvePolicy(
					AppDomain.CurrentDomain.Evidence);
		}

		/// <summary>
		/// Loads an assembly in the represented environment.
		/// </summary>
		/// <param name="fullName">The full name of the assembly.
		/// </param>
		/// <returns>A reference to that assembly.</returns>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed of.</exception>
		/// <exception cref="ArgumentNullException"><c>fullName</c> is
		/// null.</exception>
		public Assembly LoadAssembly(string fullName)
		{
			if (_disposed)
				throw new ObjectDisposedException("AppDomainEnvironment");
			if (fullName == null)
				throw new ArgumentNullException("fullName");
			return _appDomain.Load(fullName);
		}

		/// <summary>
		/// Starts the log in the new <see cref="AppDomain"/>.
		/// </summary>
		private static void StartLog()
		{
			Debug.Listeners.Add(new ConsoleTraceListener());
			Debug.AutoFlush = true;
			XmlConfigurator.ConfigureAndWatch(
				new FileInfo(
					Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
					             "Properties/Grawlix.log4net")));
			if (_log.IsInfoEnabled)
				_log.Info("Log initialized in " +
				          AppDomain.CurrentDomain);
		}

		/// <summary>
		/// Returns a <see cref="T:System.String"/> that represents the
		/// current <see cref="T:System.Object"/>.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.String"/> that represents the current
		/// <see cref="T:System.Object"/>.
		/// </returns>
		public override string ToString()
		{
			if (_disposed)
				return "[Disposed: " + GetType() + "]";
			return "AppDomainEnvironment-" + _appDomain.FriendlyName;
		}

		/// <summary>
		/// Unloads the <see cref="AppDomain"/>.
		/// </summary>
		[SuppressMessage("Microsoft.Design", "CA1031",
			Justification =
				"Exceptions shouldn't propagate past ThreadPool entry points."
			)]
		private void Unload()
		{
			if (_appDomain != null)
			{
				ThreadPool.QueueUserWorkItem(obj =>
					{
						try
						{
							AppDomain.Unload((AppDomain)obj);
						}
						catch (Exception exception)
						{
							if (_log.IsWarnEnabled)
								_log.Warn("Error during AppDomain unload.",
								          exception);
						}
					}, _appDomain);
				_appDomain = null;
			}
		}
	}
}